package repositories

import (
	"bufio"
	"encoding/csv"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net/url"
	"os"
	"path"
	"strings"
	"time"

	g "github.com/doug-martin/goqu/v9"
	"github.com/go-git/go-git/v5"
	"github.com/go-git/go-git/v5/plumbing"
	"github.com/go-git/go-git/v5/plumbing/object"
	"github.com/lib/pq"

	"gitee.com/yanxingshuyuan/backend/server/models"
	"github.com/jmoiron/sqlx"
)

var TAG_PREFIX string = "refs/tags/"
var TAG_FILE string = "tags.csv"

// mat related data repository
type MatRepo struct {
	db      *sqlx.DB
	rootDir string
	tagRepo *TagRepo
}

// NewMatRepo creator for MatRepo
func NewMatRepo(db *sqlx.DB, dir string, tagRepo *TagRepo) *MatRepo {
	return &MatRepo{db: db, rootDir: dir, tagRepo: tagRepo}
}

func (r *MatRepo) updateAuthorsForMat(pids []string, mid string, tx *sqlx.Tx) bool {
	for i := range pids {
		pid := pids[i]
		_, err := tx.Exec("INSERT INTO mats_people (mid, pid) VALUES ($1, $2) ON CONFLICT DO NOTHING", mid, pid)
		if err != nil {
			log.Printf("error adding mat:%v, with err: %v\n", mid, err)
			return false
		}

		// 在导入诗词时，需要检查作者的active字段
		_, err = tx.Exec("UPDATE people set active = true where id = $1 and active = false", pid)
		if err != nil {
			log.Printf("error setting active for person:%v, with err: %v\n", pid, err)
			return false
		}
	}
	// 删除多余的作者
	query, args, err := sqlx.In("delete from mats_people where mid = ? and pid not in (?)", mid, pids)
	if err != nil {
		log.Printf("error making in query from db when deleting authors for mat:[%#v], with err:%v\n", mid, err)
	}
	query = r.db.Rebind(query)
	_, err = tx.Exec(query, args...)
	if err != nil {
		log.Printf("error making in query from db when deleting authors for mat:[%#v], with err:%v\n", mid, err)
	}
	return true
}

func (r *MatRepo) HasMat(m *models.MatForm) bool {
	var count int64
	nstmt, err := r.db.PrepareNamed("SELECT COUNT(id) FROM mats where kind = :kind and title = :title and link = :link and source = :source")
	if err != nil {
		log.Printf("Error preparing statment for HasMat:%v, with error: %v\n", m, err)
		return false
	}
	err = nstmt.Get(&count, m)
	if err != nil {
		log.Printf("Error counting mat with form: %#v, with error: %v\n", m, err)
		return false
	}
	return count > 0
}

// 添加一份新素材
func (r *MatRepo) AddMat(m *models.MatForm) bool {
	tx, err := r.db.Beginx()
	if err != nil {
		log.Printf("error adding mat, can't create transaction: %v\n ", err)
		return false
	}

	// 先将素材本身的信息添加到数据库
	var id string
	nstmt, err := tx.PrepareNamed("INSERT INTO mats (kind, format, title, intro, content, link, source) VALUES (:kind, :format, :title, :intro, :content, :link, :source) RETURNING id")
	err = nstmt.Get(&id, m)
	if err != nil {
		log.Printf("error adding mat:%v, with err: %v\n", m, err)
		return false
	}

	// 获取新的素材ID
	m.ID = id

	// 如果前端传来了作者ID，需要更新素材到作者的映射
	pids := m.Pids
	if strings.Contains(pids, ",") {
		pidarr := strings.Split(pids, ",")
		r.updateAuthorsForMat(pidarr, m.ID, tx)
	} else {
		r.updateAuthorsForMat([]string{pids}, m.ID, tx)
	}
	// 如果前端传来了标签，需要更新素材到标签的映射
	r.tagRepo.UpdateTagsForMat(m.Tags, m.ID, tx, models.MatTableMats)

	// 如果有子节点，需要更新父子关系
	if m.Children != nil && len(m.Children) > 0 {
		fmt.Printf("children: %#v\n", m.Children)
		for i := range m.Children {
			child := m.Children[i]
			_, err = tx.Exec("INSERT INTO trees (mid, parent) VALUES ($1, $2) ON CONFLICT DO NOTHING", child.ID, m.ID)
		}
	}
	tx.Commit()
	return true
}

func (r *MatRepo) AddTree(child int64, parent int64, order int) bool {
	_, err := r.db.Exec("INSERT INTO trees (mid, parent, ord) VALUES ($1, $2, $3) ON CONFLICT ON CONSTRAINT trees_pk DO UPDATE SET ord = EXCLUDED.ord", child, parent, order)
	if err != nil {
		log.Printf("Error adding tree relation: %v, %v, with err:%v\n", parent, child, err)
		return false
	}
	return true
}

// 更新对素材的修改
func (r *MatRepo) ModifyMat(m *models.MatForm) bool {
	tx, err := r.db.Beginx()
	if err != nil {
		log.Printf("error modifying mat, can't create transaction: %v\n ", err)
		return false
	}

	_, err = tx.NamedExec("UPDATE mats set title = :title, content = :content, link = :link, source = :source, intro = :intro, modtime = now() where id = :id", m)
	if err != nil {
		log.Printf("error modifying mat:%v, with err: %v\n", m, err)
		return false
	}

	// 如果前端传来了作者ID，需要更新素材到作者的映射
	pids := m.Pids
	if strings.Contains(pids, ",") {
		pidarr := strings.Split(pids, ",")
		r.updateAuthorsForMat(pidarr, m.ID, tx)
	} else {
		r.updateAuthorsForMat([]string{pids}, m.ID, tx)
	}

	if ok := r.tagRepo.UpdateTagsForMat(m.Tags, m.ID, tx, models.MatTableMats); !ok {
		return false
	}

	tx.Commit()
	return true
}

func (r *MatRepo) GetMatByPath(p string) models.MatForm {
	// path的形式：/github.com/user/project/tree|blob/version/treepath
	var mat models.MatForm
	parts := strings.Split(p, "/")
	if len(parts) <= 2 {
		return models.MatForm{}
	}
	source := parts[1]                            // 即 github.com
	project := path.Join("/", parts[2], parts[3]) // 即 /user/project
	nodeType := parts[4]
	version := parts[5]
	treePath := strings.TrimSpace(path.Join(parts[6:]...))
	fmt.Printf("project: source: %v, name: %v\n", source, project)
	fmt.Printf("nodeType: %v, version %v, treePath: %v\n", nodeType, version, treePath)
	if treePath == "" { // 根目录
		err := r.db.Get(&mat, "SELECT * from mats where title = $1", project)
		if err != nil {
			log.Printf("error getting mat from db:%v, with err:%v\n", p, err)
		}
	} else {
		err := r.db.Get(&mat, "SELECT * from mats where title = $1 and link = $2", treePath, project)
		if err != nil {
			log.Printf("error getting mat from db:%v, with err:%v\n", p, err)
		}
	}
	// 数据库找不到素材的记录，或者找到记录之后内容为空，都要尝试从文件系统中读取path对应的文件内容
	if mat.ID == "" || mat.Content == "" {
		r.loadMatFromFile(source, project, version, treePath, &mat)
	}
	if mat.Title == "" {
		mat.Title = project
	}
	if mat.Source == "" {
		mat.Source = source
	}
	log.Printf("mat title:%v\n", mat.Title)
	r.fillAuthorsForMat(&mat, "mats")
	return mat
}

func (r *MatRepo) GetToc(mid string) (items []models.Mat) {
	err := r.db.Select(&items, "select m.* from mats m, trees t where m.id = t.mid and t.parent = $1 order by t.ord asc", mid)
	if err != nil {
		log.Printf("error getting toc for mat: %v\n", mid)
	}
	return
}

func (r *MatRepo) GetMat(mid string) models.MatForm {
	return r.getMatOrResource(mid, models.MatTableMats)
}

func (r *MatRepo) GetResource(mid string) models.MatForm {
	return r.getMatOrResource(mid, models.MatTableResources)
}

func (r *MatRepo) getMatOrResource(mid string, table models.MatTable) models.MatForm {
	var mat models.MatForm
	t := string(table)
	err := r.db.Get(&mat, "SELECT * from "+t+" where id = $1", mid)
	if err != nil {
		log.Printf("error getting mat from db:%v, with err:%v\n", mid, err)
	}
	if mat.Format == models.MatFormatFile {
		r.loadContentFromFile(&mat, "master")
	}
	r.fillAuthorsForMat(&mat, table)
	r.fillTagsForMat(&mat, table)
	return mat
}

func (r *MatRepo) SearchMatsWithChildren(kind models.MatKind, pid string) models.MatSearchResult {
	mats := r.SearchMats(models.MatSearchForm{Kind: kind, Author: pid})
	r.fillChildrenForMats(mats.Mats)
	return mats
}

func (r *MatRepo) SearchMats(search models.MatSearchForm) models.MatSearchResult {
	table := search.Table
	if table == "" {
		table = models.MatTableMats
	}
	t := string(table)
	log.Printf("table : %v\n", table)
	ms := []models.MatForm{}
	var ds *g.SelectDataset
	ds = g.From(g.T(t).As("m"))
	ds = ds.Where(g.Ex{"m.kind": search.Kind})

	fmt.Printf("search Form:%#v\n", search)

	if search.Title != "" {
		ds = ds.Where(g.Ex{"m.title": search.Title})
	}

	// 根据作者搜索
	if search.Author != "" {
		ds = ds.Join(
			g.T(t+"_people").As("mp"),
			g.On(g.Ex{"m.id": g.I("mp.mid")})).Where(g.Ex{"mp.pid": search.Author})
	}

	// 个人收藏. TODO：把Pid改为更能体现“个人收藏”的名字
	if search.Mypid != "" {
		ds = ds.Join(
			g.T("stats").As("s"),
			g.On(g.Ex{"s.mid": g.I("m.id"), "s.pid": search.Mypid}))
	}
	// 根据标签搜索
	// TODO: 暂时只支持单个标签，未来添加多个标签的组合搜索
	if search.Tag != "" {
		ds = ds.Join(
			g.T(t+"_tags").As("mt"),
			g.On(g.Ex{"m.id": g.I("mt.mid")})).Where(g.Ex{"mt.tid": search.Tag})
	}

	if search.Keyword != "" {
		// amids := r.searchMidsForAuthorName(search.Keyword, search.Table)
		if search.SearchType == models.SearchTypeTitle {
			ds = ds.Where(g.Ex{"m.title": g.Op{"like": "%" + search.Keyword + "%"}})
		} else if search.SearchType == models.SearchTypeContent {
			ds = ds.Where(g.Ex{"m.content": g.Op{"like": "%" + search.Keyword + "%"}})
		} else if search.SearchType == models.SearchTypeAuthor {
			ds = ds.Join(g.T(t+"_people").As("mp"),
				g.On(g.Ex{"m.id": g.I("mp.mid")}))
			ds = ds.Join(g.T("people").As("p"),
				g.On(g.Ex{"mp.pid": g.I("p.id")})).Where(g.Ex{"p.fullname": g.Op{"like": "%" + search.Keyword + "%"}})
		}
	}

	// 总数
	dc := ds.Select(g.COUNT(g.I("m.id")))
	sqlc, _, _ := dc.ToSQL()
	fmt.Printf("count sql:%v\n", sqlc)
	var totalCount int64
	err := r.db.Get(&totalCount, sqlc)
	if err != nil {
		fmt.Printf("error counting mats:%v\n", err)
	}

	ds = ds.Select("m.*")
	ord := g.I("m.modtime").Desc()
	if search.Orderby == "f" { // 收藏时间，favor
		ord = g.I("n.modtime").Desc() // 注意，这里先决条件是有个人收藏，否则会出错
	} else if search.Orderby == "n" { // 名称
		ord = g.I("m.title").Asc()
	}
	ds = ds.Order(ord)
	if search.Random {
		ds = ds.OrderPrepend(g.L("random()").Desc())
	}

	// 分页功能
	if search.Limit > 0 {
		ds = ds.Limit(search.Limit)
		if search.Offset > 0 {
			ds = ds.Offset(search.Offset)
		}
	}
	sql, _, _ := ds.ToSQL()
	log.Printf("SQL:%v\n", sql)
	err = r.db.Select(&ms, sql)
	if err != nil {
		log.Printf("error searching mats from db:%#v, with err:%v\n", search, err)
	}
	r.fillTagsForMats(ms, table)
	r.fillAuthorsForMats(ms, table)
	if table == "resources" {
		for i := range ms {
			ms[i].IsResource = true
		}
	}
	return models.MatSearchResult{
		Total: totalCount,
		Mats:  ms,
	}
}

func (r *MatRepo) searchMidsForAuthorName(name string, table models.MatTable) (mids []string) {
	t := string(table)
	err := r.db.Select(&mids, "select mp.mid from "+t+"_people mp, people p where mp.pid = p.id and p.fullname like $1", "%"+name+"%")
	if err != nil {
		log.Printf("Error searching for mids with author name %v, with error:%v\n", name, err)
	}
	return
}

func (r *MatRepo) fillTagsForMat(mat *models.MatForm, table models.MatTable) {
	t := string(table)
	query, args, err := sqlx.In("SELECT t.* from "+t+" m, "+t+"_tags mt, tags t where m.id = mt.mid and t.id = mt.tid and m.id = ?", mat.ID)
	if err != nil {
		log.Printf("error making in query from db when filling tags for mat:[%#v], with err:%v\n", mat.ID, err)
	}
	query = r.db.Rebind(query)
	err = r.db.Select(&mat.Tags, query, args...)
	if err != nil {
		log.Printf("error getting tags for mat:[%#v], with err:%v\n", mat.ID, err)
	}
}

func (r *MatRepo) fillTagsForMats(mats []models.MatForm, table models.MatTable) {
	for i := range mats {
		r.fillTagsForMat(&mats[i], table)
	}
}

func (r *MatRepo) fillChildrenForMats(mats []models.MatForm) {
	for i := range mats {
		mid := mats[i].ID
		var children []models.Mat
		err := r.db.Select(&children, "SELECT m.* from mats m, trees t where m.id = t.mid and t.parent = $1", mid)
		if err != nil {
			log.Printf("error getting children for mat: %d, with error:%v\n", mid, err)
		}
		mats[i].Children = children
	}
}

func (r *MatRepo) fillAuthorsForMats(mats []models.MatForm, table models.MatTable) {
	for i := range mats {
		r.fillAuthorsForMat(&mats[i], table)
	}
}

func (r *MatRepo) fillAuthorsForMat(mat *models.MatForm, table models.MatTable) {
	mid := mat.ID
	var authors []models.Person
	mp := string(table) + "_people"
	err := r.db.Select(&authors, "SELECT p.* from "+string(table)+" m, people p,"+mp+" mp where m.id = mp.mid and mp.pid = p.id and m.id = $1", mid)
	if err != nil {
		log.Printf("error getting authors for mat: %v, with error:%v\n", mid, err)
	}
	mat.Authors = authors
}

func (r *MatRepo) ListMats(pid string) []models.Mat {
	mats := []models.Mat{}
	err := r.db.Select(&mats, "SELECT m.*, p.pid from mats m, mats_people mp where m.id = mp.mid and mp.pid = $1 order by m.modtime desc", pid)
	if err != nil {
		log.Printf("error listing mats from db:%v, with err:%v\n", pid, err)
	}
	return mats
}

func (r *MatRepo) ListMatsInCollection(pid string, colid string) []models.Mat {
	var artids []string
	err := r.db.Select(&artids, "SELECT artid from collections_mats where colid = $1", colid)
	if err != nil {
		log.Printf("error listing mats for collection from db:%v, with err:%v\n", colid, err)
	}

	// TODO: check if using pq.Array and ANY(?) is the best option here
	mats := []models.Mat{}
	err = r.db.Select(&mats, "SELECT a.id, a.title, a.modtime, p.pid from entries a, entries_people p where a.id = p.aid and p.pid = $1 and a.id = ANY($2) order by a.modtime desc", pid, pq.Array(artids))
	if err != nil {
		log.Printf("error listing mats from db:%v, with err:%v\n", pid, err)
	}
	return mats
}

func (r *MatRepo) RemoveMat(mid string) bool {
	tx := r.db.MustBegin()
	_, err := tx.Exec("DELETE FROM mats where id = $1", mid)
	if err != nil {
		log.Printf("error removing mat from db:%v, with err:%v\n", mid, err)
		return false
	}
	// 删除作者关系
	_, err = tx.Exec("DELETE FROM mats_people where mid = $1", mid)
	if err != nil {
		log.Printf("error removing mats_people relation from db:%v, with err:%v\n", mid, err)
		return false
	}

	// 删除标签关系
	_, err = tx.Exec("DELETE FROM mats_tags where mid = $1", mid)
	if err != nil {
		log.Printf("error removing mats_tags relation from db:%v, with err:%v\n", mid, err)
		return false
	}

	// TODO: 如果有必要，删除父子节点关系？
	tx.Commit()
	return true
}

func (r *MatRepo) AddResource(m *models.MatForm) bool {
	tx, err := r.db.Beginx()
	if err != nil {
		log.Printf("error adding mat, can't create transaction: %v\n ", err)
		return false
	}

	// 先将素材本身的信息添加到数据库
	var id string
	nstmt, err := tx.PrepareNamed("INSERT INTO resources (kind, title, intro, content, link, source, checksum) VALUES (:kind, :title, :intro, :content, :link, :source, :checksum) ON CONFLICT DO NOTHING RETURNING id")
	err = nstmt.Get(&id, m)
	if err != nil {
		// log.Printf("error adding resource:%v, with err: %v\n", m.Title, err)
		err := tx.Get(&id, "select id from resources where title = $1 and content = $2", m.Title, m.Content)
		if err != nil {
			log.Printf("error getting dup resource, %v\n", err)
		}
	}

	if id == "" {
		log.Printf("error inserting resource %v, rollback\n", m)
		tx.Rollback()
		return false
	}

	// 获取新的素材ID
	m.ID = id

	// 如果前端传来了作者ID，需要更新素材到作者的映射
	pids := m.Pids
	if strings.Contains(pids, ",") {
		pidarr := strings.Split(pids, ",")
		for i := range pidarr {
			pid := pidarr[i]
			_, err = tx.Exec("INSERT INTO resources_people (mid, pid) VALUES ($1, $2) ON CONFLICT (mid, pid) DO NOTHING", m.ID, pid)
			if err != nil {
				log.Printf("error adding mat:%v, with err: %v\n", m, err)
				return false
			}
		}

	} else {
		_, err = tx.Exec("INSERT INTO resources_people (mid, pid) VALUES ($1, $2) ON CONFLICT (mid, pid) DO NOTHING", m.ID, pids)
		if err != nil {
			log.Printf("error adding mat:%v, with err: %v\n", m, err)
			return false
		}
	}
	// 如果前端传来了标签，需要更新素材到标签的映射
	r.tagRepo.UpdateTagsForMat(m.Tags, m.ID, tx, models.MatTableResources)

	// 如果有子节点，需要更新父子关系
	if m.Children != nil && len(m.Children) > 0 {
		fmt.Printf("children: %#v\n", m.Children)
		for i := range m.Children {
			child := m.Children[i]
			_, err = tx.Exec("INSERT INTO resource_trees (mid, parent) VALUES ($1, $2)", child.ID, m.ID)
		}
	}
	tx.Commit()
	return true
}

func IsDir(path string) (bool, error) {
	stat, err := os.Stat(path)
	if err != nil {
		return false, err
	}
	return stat.IsDir(), nil
}

func getSource(uri string) string {
	u, err := url.Parse(uri)
	if err != nil {
		return uri
	}
	return u.Hostname()
}

func (r *MatRepo) getGitCommit(project string, version string) (*git.Repository, *object.Commit) {
	gitRepo, err := git.PlainOpen(path.Join(project, "bare"))
	if err != nil {
		log.Printf("Error opening git repo %v, with error: %v\n", project, err)
		return nil, nil
	}
	var ref *plumbing.Reference
	if version == "master" {
		ref, err = gitRepo.Head()
	} else {
		ref, err = gitRepo.Tag(version)
		tag, err := gitRepo.TagObject(ref.Hash())
		if err == nil { // annotated tag，从tag中获取对应commit对象
			commit, err := tag.Commit()
			if err != nil {
				log.Printf("Error getting commit for tag:%v, error: %v\n", tag, err)
			}
			return gitRepo, commit
		}
		// else: 如果无法获取TagObject，表示这是一个普通tag，而非annotated tag，只要继续当做普通的ref处理即可
	}
	log.Printf("ref for tag: %v\n", ref.String())
	if err != nil {
		log.Printf("Error opening git tag %v, with error: %v\n", version, err)
	}

	commit, err := gitRepo.CommitObject(ref.Hash())
	if err != nil {
		log.Printf("Error opening git commit %v/%v@%v, with error: %v\n", project, version, ref.String(), err)
	}
	log.Printf("commit for tag: %v\n", commit)
	return gitRepo, commit
}

func (r *MatRepo) fillGitContentForMat(mat *models.MatForm, project string, version string, treepath string) {
	gitRepo, commit := r.getGitCommit(project, version)
	if commit == nil {
		return
	}
	file, err := commit.File(treepath)
	if err != nil {
		log.Printf("Error opening git file %v/%v/%v, with error: %v\n", project, version, treepath, err)
	}
	if file == nil {
		mat.Format = models.MatFormatDir
		mat.Kind = models.KindDir
		mat.Link = "@" + treepath
		mat.Entries = r.getGitEntriesForCommit(gitRepo, commit, treepath)
	} else {
		log.Printf("Got file: %v\n", file)
		log.Printf("file is file: %v\n", file.Mode.IsFile())
		content, err := file.Contents()
		if err != nil {
			log.Printf("Cant get git file content: %v/%v/%v, error: %v\n", project, version, treepath, err)
		}
		mat.Format = models.MatFormatFile
		mat.Kind = models.KindFile
		mat.Link = "@" + treepath
		mat.Content = content
	}
}

func (r *MatRepo) GetGitEntries(project string, version string, treepath string) (files []models.File) {
	gitRepo, commit := r.getGitCommit(project, version)
	if commit == nil {
		return
	}
	files = r.getGitEntriesForCommit(gitRepo, commit, treepath)
	return
}

func (r *MatRepo) getGitEntriesForCommit(gitRepo *git.Repository, commit *object.Commit, treepath string) (files []models.File) {
	treeObj, err := commit.Tree()
	if err != nil {
		log.Printf("Error opening git tree %v, with error: %v\n", commit, err)
	}
	log.Printf("openning tree for path: %v\n", treepath)
	if treepath != "" {
		treeObj, err = treeObj.Tree(treepath)
	}
	if err != nil {
		log.Printf("Error opening git tree path %v, with error: %v\n", treepath, err)
	}
	intro := "简介"
	t := time.Now()
	for _, f := range treeObj.Entries {
		// TODO: 处理其他类型的隐藏文件夹或系统文件夹
		if f.Name == ".git" {
			continue
		}
		files = append(files, models.File{
			Name:    f.Name,
			IsDir:   !f.Mode.IsFile(),
			Intro:   intro,
			ModTime: t,
		})
		fmt.Printf("blob %s    %s\n", f.Hash, f.Name)
	}
	return
}

func (r *MatRepo) ImportProject(form *models.ProjectImportForm) (tags []models.GitTag) {
	// TODO: 为了避免这个功能被利用来攻击网站算力，最好将所有克隆操作串行起来排队作业，并处理重复情况。
	// 以及，要处理短时间内大量请求的情况，最好设置一个“某时间段内只能进行N词克隆作业”的上限
	// 另外，还可以考虑对单个用户的连续多次克隆操作增加星星费用。

	// 首先建立工程目录
	projDir := path.Join(r.rootDir, "projects", form.Source, form.Name)
	err := os.MkdirAll(projDir, os.ModePerm)
	if err != nil {
		log.Printf("Error creating project dir:%#v, with error:%v\n", form, err)
	}
	// 创建bare仓库（这个仓库只有git信息，而没有文件内容）
	masterDir := path.Join(projDir, "bare")

	var repo *git.Repository

	if ok, _ := IsDir(masterDir); ok {
		repo, err = git.PlainOpen(masterDir)
	} else {
		repo, err = git.PlainClone(masterDir, true, &git.CloneOptions{
			URL:      form.URL,
			Progress: os.Stdout,
			// NoCheckout: true, // 不检出文件，这样可以节省大量空间。等到用户具体浏览文件时，再将文件检出
			// Depth:    1, // TODO: 搞清楚怎么做才能既不影响内容，又能不占空间
		})
	}

	if err != nil {
		fmt.Printf("Error cloning master repo: %#v, %v\n", form, err)
		return
	}

	// 扫描代码库，获取所有版本tag
	tagFile, err := os.Create(path.Join(projDir, TAG_FILE))
	if err != nil {
		log.Printf("error creating tags.csv for %v\n", projDir)
	}
	defer tagFile.Close()
	w := bufio.NewWriter(tagFile)
	tagRefs, err := repo.Tags()
	err = tagRefs.ForEach(func(t *plumbing.Reference) error {
		log.Println(t)
		tags = append(tags, models.GitTag{
			Name: t.Name().String(),
			Hash: t.Hash().String(),
		})
		var msg string
		tagObj, err := repo.TagObject(t.Hash())
		switch err {
		case nil:
			msg = tagObj.Message
		}
		_, err = w.WriteString(t.Name().String())
		_, err = w.WriteString("\t")
		_, err = w.WriteString(t.Hash().String())
		if msg != "" {
			msg = strings.ReplaceAll(msg, "\n", "\\n")
			_, err = w.WriteString("\t")
			_, err = w.WriteString(msg)
		}
		_, err = w.WriteString("\n")
		return nil
	})
	w.Flush()

	// 如果没有稳定版本，则自动生成一个tag，以便于未来代码更新后找不到
	if len(tags) == 0 {
		// TODO: 生成一个初始tag
	}

	// 把克隆的工程转化为一个mat加入到数据库中
	mat := &models.MatForm{}
	mat.Kind = models.KindProject
	mat.Format = models.MatFormatProject
	mat.Title = form.Name
	mat.Intro = form.Desc
	mat.Link = form.URL
	mat.Source = getSource(form.URL)
	if r.HasMat(mat) {
		return
	}
	r.AddMat(mat)
	return
}

func (r *MatRepo) loadContentFromFile(mat *models.MatForm, version string) {
	link := mat.Link
	if link == "" || !strings.HasPrefix(link, "@") {
		return
	} else {
		loc := link[1:]
		idx := strings.Index(loc, "/")
		if idx >= 0 {
			bookname := loc[:idx]
			filepath := loc[idx:]
			if version == "" {
				version = "master"
			}
			p := path.Join(r.rootDir, "books", bookname, version, filepath)
			t, err := ioutil.ReadFile(p)

			if err != nil {
				log.Printf("Error reading content for file: %v, %v\n", mat.Title, mat.Link)
				return
			}
			mat.Content = string(t)
		}
	}
}

func (r *MatRepo) loadMatFromFile(source string, project string, version string, filepath string, mat *models.MatForm) {
	p := path.Join(r.rootDir, "projects", source, project, version, filepath)
	log.Printf("loading mat content from file %v\n", p)
	s, err := os.Stat(p)
	if err != nil {
		fmt.Printf("Error reading stats for file: %v\n", p)
		// 文件找不到，需要尝试从git中获取信息
		projectPath := path.Join(r.rootDir, "projects", source, project)
		r.fillGitContentForMat(mat, projectPath, version, filepath)
		return
	}
	if s.IsDir() {
		if mat.Format == 0 {
			mat.Format = models.MatFormatDir
			mat.Kind = models.KindDir
			mat.Link = "@" + filepath
		}
		mat.Entries = r.ListDir(source, project, version, filepath)
	} else {
		t, err := ioutil.ReadFile(p)
		if err != nil {
			fmt.Printf("Error reading content for file: %v\n", p)
			return
		}
		mat.Format = models.MatFormatFile
		mat.Kind = models.KindFile
		mat.Link = "@" + filepath
		mat.Content = string(t)
	}
	return
}

func (r *MatRepo) ListDir(source string, project string, version string, filepath string) (files []models.File) {

	// p := path.Join(r.rootDir, "projects", dir)
	// TODO: 从直接读取文件改为读取Git里的信息
	projectPath := path.Join(r.rootDir, "projects", source, project)
	files = r.GetGitEntries(projectPath, version, filepath)

	return
}

func (r *MatRepo) AddMatWithPath(path string) (mat models.MatForm) {
	parts := strings.Split(path, "/")
	fmt.Printf("parts split into: %#v\n", parts)
	if len(parts) < 3 {
		return
	}

	// 先找到路径对应的工程
	// 工程的路径形式为/user/repo，即parts[0:3]
	projectPath := strings.Join(parts[0:3], "/")
	fmt.Printf("project path: %v\n", projectPath)
	mat = r.GetMatByPath(projectPath)
	if mat.ID == "" {
		// TODO: 可以考虑在这里直接新建一个工程，而不用依赖于ImportProject函数
		fmt.Printf("mat for project %v not found.\n", projectPath)
		return
	}
	fmt.Printf("project: %#v\n", mat)

	// 从工程开始向下搜索各层目录对应的素材，如果找不到则依次添加到数据库
	parent := mat
	n := 4
	for n <= len(parts) {
		p := strings.Join(parts[0:n], "/")
		fmt.Printf("current path: %v\n", p)
		mat = r.GetMatByPath(p)
		format := models.MatFormatDir
		if n == len(parts) {
			format = models.MatFormatFile
		}
		if mat.ID == "" {
			form := models.MatForm{
				Mat: models.Mat{
					Kind:   models.KindDir,
					Title:  p,
					Link:   parent.Mat.Link,
					Source: parent.Mat.Source,
					Format: format,
				},
			}
			fmt.Printf("adding mat: %#v\n", form)
			r.AddMat(&form)
			mat = form
		}
		parent = mat
		n = n + 1
	}
	return
}

func (r *MatRepo) MatStats(kind models.MatKind) models.PoemStat {
	stats := models.PoemStat{}

	tx, err := r.db.Beginx()
	if err != nil {
		fmt.Printf("error creating transaction: %v\n", err)
	}
	err = tx.Get(&stats.Fav, "select count(*) from mats m, stats s where m.id = s.mid and m.kind = $1", kind)
	if err != nil {
		fmt.Printf("error getting fav stats: %v\n", err)
		tx.Rollback()
		return stats
	}

	err = tx.Get(&stats.Discover, "select count(*) from mats where kind = $1", kind)
	if err != nil {
		fmt.Printf("error getting discover stats: %v\n", err)
		tx.Rollback()
		return stats
	}

	err = tx.Get(&stats.Resources, "select count(*) from resources where kind = $1", kind)
	if err != nil {
		fmt.Printf("error getting discover stats: %v\n", err)
		return stats
	}
	tx.Commit()
	return stats
}

func (r *MatRepo) GetProjectVersions(source string, org string, proj string) []models.Version {
	name := path.Join(org, proj)
	return r.getVersions(source, name)
}

func (r *MatRepo) GetMatVersions(mid string) []models.Version {
	mat := r.GetMat(mid)
	source := mat.Source
	name := mat.Title
	return r.getVersions(source, name)
}

func (r *MatRepo) getVersions(source string, name string) (versions []models.Version) {
	projDir := path.Join(r.rootDir, "projects", source, name)
	tagsFile := path.Join(projDir, TAG_FILE)

	f, err := os.Open(tagsFile)
	if err != nil {
		log.Printf("Cannot open project version file: %v\n", tagsFile)
		return
	}
	defer f.Close()

	rd := csv.NewReader(f)
	rd.Comma = '\t'
	for {
		row, err := rd.Read()
		if err != nil && err != io.EOF {
			log.Printf("error reading csv file %v\n", err)
		}
		if err == io.EOF {
			break
		}
		log.Printf("Got row:%#v\n", row)
		v := models.Version{
			Name: row[0],
			Hash: row[1],
		}
		if len(row) > 2 {
			v.Desc = row[2]
		}
		if strings.HasPrefix(v.Name, TAG_PREFIX) {
			v.Name = v.Name[len(TAG_PREFIX):]
		}
		versions = append(versions, v)
	}
	return
}
